
"""
CoordPlane, or CoordinatePlane
An infinite plane that contains objects at various locations. 
"""

import logging
from pgEngine.point import *
from utils import Identified

class CollisionError(Exception):
	""" Represents an attempt to move one object into another. """
	def __init__(self, collider, colliderLoc, collidee, collideeLoc, **kwargs):
		super(CollisionError, self).__init__(**kwargs)
		self.collider = collider
		self.colliderLoc = colliderLoc
		self.collidee = collidee
		self.collideeLoc = collideeLoc

class CoordPlane(Identified):
	"""
	Maintains the locations of game objects, and the landscape. Not simply
	the World class because there might be some World-specific feature that
	isn't implemented just yet.
	"""
	items = {}
	locations = {}
	game = None

	logger = logging.getLogger("engine.CoordPlane")

	def __new__(cls, itemLocs=None, game=None):
		if itemLocs is None: itemLocs = {}
		self = super(CoordPlane, cls).__new__(cls)
		for item, coord in itemLocs:
			self.items[item] = coord
			self.locations[coord] = item
		self.game = game
		return self

	def __str__(self):
		return '<CoordPlane(%i) at %s>' % (len(self.items), id(self))
	
	def __repr__(self):
		return 'CoordPlane(%r)' % (self.items)
	
	# Game Objects
	def moveItem(self, item, vector):
		""" Move an object by a relative vector. """
		if item not in self.items:
			raise ValueError, "Item does not yet exist in CoordPlane: %s" % item
		
		oldLoc = self.items[item]
		newLoc = oldLoc + vector
		if newLoc.loc in self.locations:
			collidee = self.locations[newLoc.loc]
			if collidee is not item:
				raise CollisionError(item, newLoc, collidee, 
									self.items[collidee])
		self.items[item] = newLoc
		del self.locations[oldLoc.loc]
		self.locations[newLoc.loc] = item
		
		return newLoc
		
	def moveItemTo(self, item, point):
		""" Move an item to a specific location. """
		if point.loc in self.locations:
			collidee = self.locations[point.loc]
			raise CollisionError(item, point,
								collidee, self.items[collidee])
		elif item in self.items:
			oldLoc = self.items[item]
			del self.locations[oldLoc.loc]
			
		self.items[item] = point
		self.locations[point.loc] = item
		
	def append(self, item, point):
		""" Add a non-pre-extant item. """
		if item in self.items:
			raise ValueError, "Item already exists in CoordPlane: %s" % item
		self.moveItemTo(item, point)

	def getCoord(self, item):
		""" Return the location of a specific object. """
		return self.items[item]

	def getNearestItem(self, source, targetType):
		least = None
		nearestItem = None
		sourceLoc = self.items[source]
		for item,loc in self.items.items():
			if type(item) is not targetType: continue
			dist = sourceLoc.dist(loc)
			if least is None or dist < least:
				nearestItem = item
				least = dist
		return nearestItem
	
	def getItems(self, x, y, w, h):
		x2 = x + w
		y2 = y + h
		items = [ (loc,item) for item,loc in self.items.items()
							if loc.x >= x and loc.x <= x2 and
								loc.y >= y and loc.y <= y2 ]
		return items

	def checkCollision(self, location):
		if location.loc in self.locations:
			collidee = self.locations[location.loc]
			return (collidee, self.items[collidee])
		return None
	
	def getDistance(self, source, target):
		p1, p2 = self.getCoord(source), self.getCoord(target)
		return ((p1.x-p2.x)**2 + (p1.y-p2.y)**2)**.5

class CoordPoly(Identified):
	"""
	A coordinate plane that ensures all of its contents stay inside the plane's bounds.
	Bounds are defined by a set of points given in order. 
	"""
	bounds = None
	game = None
	items = None
	locations = None
	allowedType = object
	def __new__(cls, boundPoints, itemLocs=None, game=None, typeAllowed=object):
		if itemLocs is None: itemLocs = {}
		self = super(CoordPoly, cls).__new__(cls, itemLocs, game)
		self.bounds = BoundingPoly(boundPoints)
		self.allowedType = typeAllowed
		for loc, item in self.locations:
			if not self.bounds.collision(loc) or not isinstance(item, allowedType):
				#Remove the item.
				del self.locations[loc]
				del self.items[item]
		return self
	
	def __validate(self, item, location):
		"""
		Ensure that the purposed change could work. Does not raise an exception.
		"""
		#TODO: Add collision checking!
		return self.bounds.collision(location) and isinstance(item, self.allowedType)
	
	def moveItem(self, item, vector):
		oldLoc = self.items[item]
		newLoc = oldLoc + vector
		if not self.__validate(item, newLoc):
			return False
		
		if newLoc in self.locations:
			collidee = self.locations[newLoc]
			if collidee is not item:
				raise CollisionError(item, newLoc, collidee, 
									self.items[collidee])
		self.items[item] = newLoc
		del self.locations[oldLoc]
		self.locations[newLoc] = item
		
		return newLoc
	
	def moveItemTo(self, item, point):
		""" Move an item to a specific location. """
		if not self.__validate(item, point): return False
		if point in self.locations:
			collidee = self.locations[point]
			raise CollisionError(item, point,
								collidee, self.items[collidee])
		elif item in self.items:
			oldLoc = self.items[item]
			del self.locations[oldLoc]
			
		self.items[item] = point
		self.locations[point] = item
	
	def append(self, item, loc):
		""" Add a non-pre-extant item. """
		if item in self.items:
			raise ValueError, "Item already exists in CoordPoly: %s" % item
		self.moveItemTo(item, point)
	
	def getCoord(self, item):
		""" Return the location of a specific object. """
		return super(CoordPoly, self).getCoord(item)
	
	def getNearestItem(self, source, type):
		return super(CoordPoly, self).getNearestItem(source, type)
	
	def getItems(self, x, y, w, h):
		return super(CoordPoly, self).getItems(x, y, w, h)
	
	def checkCollision(self, location):
		return super(CoordPoly, self).checkCollision(location)
	
	def getDistance(self, source, target):
		return super(CoordPoly, self).getDistance(source, target)
	
